import os
import math
import pyglet

from actors import ActorFactory
from map_load_helper import get_actors
import utils as ut

import gg
fe = 0.001

# using complex coords

class World(pyglet.event.EventDispatcher):        
    def __init__(self, world_creation_info):
        super(World,self).__init__()
        # make available params from game mode
        self.gm_game_mode = world_creation_info['game_mode']
        self.gm_pigeons_nowayback = world_creation_info['pigeons_nowayback']
        self.gm_birds_nowayback = world_creation_info['birds_nowayback']
        self.next_stage = world_creation_info['stage']
        self.world_creation_info = world_creation_info 
        # set constants
        self.height = gg.world_height
        #self.width <- calc from map
        self.viewport_width = gg.world_viewport_width
        self.scroll_vel = gg.scroll_vel
        self.goal_width = gg.goal_width
        self.gap = gg.interstage_gap
        self.player_r = gg.player_r
        self.pigeon_r = gg.pigeon_r

        self._idx = 0
        self.stage = 0
        self.score = 0
        self.ox = 0.0
        self.first_free_org = 0.0
        self.actors = {} # actor.idx : actor 
        self.terrain = None
        self.goal = None
        self.game_terminated = False
        self.paused = True
        self.restarting = True

    def get_dir(self, selector):
        if selector == 'levels':
            fdir = os.path.join(gg.data_dir, 'levels', self.gm_game_mode)
        return fdir

    def set_controller(self, control):
        self.controller = control

    def start(self):
        # the view is ready, lets build the world
        self.next_free_org = 0.0
        self.ox = 0.0
        self.time = 0.0
        self.dt = 0.1
        self.dt_cap = gg.dt_cap
        self.birds = set() # stored only idx
        self.pigeons = ut.CategoryCollection()
        self.add_stage_actors(self.next_stage)
        self.stage = self.next_stage
        self.dispatch_event("on_pov_changed",self.ox)
        self.restarting = False

    def end(self):
        #? sumary screen , later
        print 'detected end'
        self.dispatch_event("on_end_stage")
        

    def clean_start(self):
        self.restarting = True
        # delete all actors and view actors
        for idx in self.actors.keys():
            actor = self.actors.pop(idx)
            actor.world = None
            self.dispatch_event("on_actor_deleted", idx)
        self.start()

    def restart_stage(self):
        self.next_stage = self.stage
        self.clean_start()

    def warp_next_stage(self):
        self.next_stage = self.stage + 1
        self.clean_start()

    def new_id(self):
        self._idx += 1
        return self._idx
    
    def remove_actor(self, actor):
        idx = actor.idx
        print '\nremove idx:',idx
        actor.world = None
        if actor.atype == 'bird':
            self.birds.remove(actor)
        if actor.atype == 'pigeon':
            self.pigeons.remove(actor)
        del self.actors[idx]
        self.dispatch_event("on_actor_deleted", idx)

    def actor_changed(self, actor):
        self.dispatch_event("on_actor_changed", actor.idx, actor.info_update())

    def add_actor(self, common_params, xtra_params):
        idx = self.new_id()
        common_params['idx'] = idx
        actor = ActorFactory(self, common_params, xtra_params)
        self.actors[idx] = actor
        self.dispatch_event("on_actor_added", actor.info_creation(), actor.info_update())
        return idx

    def add_stage_actors(self, stage):
        # supports out-of-order loads
        
        # level storage conventions:
        #   dir is data\levels\<gm_game_mode>
        #   for the game to run:
        #       dd.precrop.svgz, the game only needs the layer 'actors'
        #       dd.collision.png : terrain characteristics by color
        #       dd.jpg : terrain view as bitmap
        #   for the painter and collision bitmap generator
        #       dd.msk_raw.png
        #       dd.crop.png
        # see notes\workflow_level.txt
        
        org = complex(self.next_free_org, 0.0)
        fname = os.path.join(self.get_dir('levels'), '%02d.precrop.svgz'%stage)
        actors_by_colors = get_actors(fname, gg.mapactors_to_color4['terrain'])
        
        # transcode the table actors by color to actor by mapactor name
        mapactors = {}
        for m_name in gg.mapactors_to_color4:
            #print '\tm_name:',m_name
            color4 = gg.mapactors_to_color4[m_name]
            try:
                mapactors[m_name] = actors_by_colors[color4]
            except KeyError:
                print '\t no actor with type:', m_name
                # the map dont have this actor type
                pass 
        del actors_by_colors

        m_name = 'terrain'
        group = mapactors.pop(m_name)
        atype= gg.params_for_mapactors[m_name][0]
        getattr(self, 'add_group_' + atype)(org, m_name, group, stage)            

        for m_name in mapactors:
            group = mapactors[m_name]
            atype = gg.params_for_mapactors[m_name][0]
            getattr(self, 'add_group_' + atype)(org, m_name, group)            

        self.add_pusher_line(org)
        self.next_stage = stage
        self.next_free_org += self.width 

    def add_group_terrain(self, org, m_type, group, stage):
        atype, subtype = gg.params_for_mapactors[m_type] 
        if len(group)==0:
            print 'WARN: no terrain actor at stage %d.'%stage 
        pos, width, height = group[0]
        # reasonability warns
        if len(group)>1:
            print 'WARN: detected more than one terrain at stage %d.'%stage 
        if width<self.viewport_width:
            print 'WARN: world width < viewport_width at stage %d.'%stage
            print '\twidth, viewport_width:'%(width, self.viewport_width)
        if height != self.height:
            print 'WARN: world height != terrain width at stage %d.'%stage
            print '\t world.height , terrain height:', self.height, height 
        self.width = width
        self.scrolling = True
        self.scroll_limit = self.width + gg.scroll_overshot - self.viewport_width
        ccp = {
            'pos': complex(org, 0.0),
            'atype': atype,
            'subtype': subtype,
            'collision_radius': width/2.0
            }
        xtra = {'stage':stage}
        idx = self.add_actor(ccp, xtra)
        self.terrain = self.actors[idx]

    def add_group_bird(self, org, m_type, group):
        # will get one group for each of player1, player2, NPCclass_xx
        atype, subtype = gg.params_for_mapactors[m_type]
        if subtype.startswith('player'):
            assert len(group)==1
            team = int(subtype[-1])-1
            if team==1 and self.gm_game_mode=='sp':
                return
            print "team:", team
            mind_type = self.world_creation_info['players'][team]['mind_type']
            button_group = self.world_creation_info['players'][team]['button_group']
        else:
            # NPCs
            mind_type = subtype
            button_group = None
        xtra = {
            'mind_type': mind_type,
            'button_group': button_group
            }

        for pos, width, height in group:
            ccp = {
                'atype': atype,
                'subtype': subtype,
                'collision_radius': self.player_r,
                'pos': pos + org
                }
            idx = self.add_actor(ccp, xtra)
            self.birds.add(self.actors[idx])

    def add_group_pigeon(self, org, m_type, group):
        atype, subtype = gg.params_for_mapactors[m_type] 
        for pos, width, height in group:
            ccp = {
                'atype': atype, 
                'subtype': subtype,
                'collision_radius': self.pigeon_r,
                'pos': pos + org
                }
            idx = self.add_actor(ccp, {})
            # auto added to self.pigeons in the pigeon init

    def add_group_goal_zone(self, org, m_type, group):
        # goal zone: discard the cx comp and place at end of stage x
        self.add_goal_line(org)
        atype, subtype = gg.params_for_mapactors[m_type] 
        for pos, width, height in group:
            ccp = {
                'atype': atype,
                'subtype': subtype,
                'collision_radius': self.gap/2.0,
                'pos':  complex(self.width, 0.0) + org
                }
            xtra = {'min_y': pos.imag - height/2.0,
                    'max_y': pos.imag + height/2.0}
            idx = self.add_actor(ccp, xtra)
            self.goal_line.add_goal_zone(self.actors[idx])

    def add_goal_line(self,org):
        # not loaded from map, generated based in world dimensions
        # must be called before adding goal_zones
        ccp = {
            'atype': 'goal_line',
            'subtype': '',
            'collision_radius': self.gap/2.0,
            'pos':  complex(self.width, 0.0) + org
            }
        xtra = {'min_y': 0.0,
                'max_y': self.height}
        idx = self.add_actor(ccp, xtra)
        self.goal_line = self.actors[idx]

    def add_pusher_line(self,org):
        ccp = {
            'atype': 'pusher_line',
            'subtype': '',
            'collision_radius': self.gap/2.0,
            'pos':  complex(0.0, 0.0) + org
            }
        idx = self.add_actor(ccp, {})
        self.pusher_line = self.actors[idx]

    def add_to_score(self, n):
        self.score += n
        self.dispatch_event("on_stats_changed",self.score, self.stage)
       
    def game_ends(self, bWin):
        self.game_terminated = True
        #self.pause = True # name clash with cocosnode member
        self.allow_next_level = self.time + 3.0
        gg.stats = { 'bWin':bWin, 'score':self.score }#, 'stage':stage }
        #self.controller.on_pause_changed(self.pause)
        self.dispatch_event("on_game_over",bWin)

    def update(self, dt):
        if self.game_terminated:
            self.time += dt
            if self.allow_next_level < self.time:
                self.dispatch_event('on_next_stage')
            return
        wbuttons = self.controller.buttons['world']
        if self.paused:
            if wbuttons['end_help']:
                wbuttons['end_help'] = 0
                wbuttons['begin_help'] = 0
                self.paused = False
                self.controller.on_pause_changed(self.paused)
                self.dispatch_event("on_help_end")
            return
        if wbuttons['begin_help'] and not self.paused:
            self.paused = True
            self.controller.on_pause_changed(self.paused)
            self.dispatch_event("on_help_request")
            return
        if wbuttons['restart'] and not self.restarting:
            self.restarting = True
            self.restart_stage()
            return
        if wbuttons['warp'] and not self.restarting:
            self.restarting = True
            self.warp_next_stage()
            return
        
        # update time
        if dt>self.dt_cap:
            dt = self.dt_cap
        self.time += dt
        self.dt = dt
            
        # update scroll
        if self.scrolling:
            self.ox += self.scroll_vel*dt
            if self.ox > self.scroll_limit:
                self.scrolling = False
            self.dispatch_event("on_pov_changed",self.ox)
        self.pusher_line.pos += self.scroll_vel*dt
        self.actor_changed(self.pusher_line)

        actors = self.actors

        # blind move for all actors
        for idx in actors:
            actors[idx].blind_move()

        # resolve bird-bird colisions
        cnt = 0
        have_collision = True
        while cnt<5 and have_collision:
            have_collision = False
            for actor in self.birds:
                if not actor.alive:
                    continue
                for other in self.birds:
                    if not other.alive or other.idx == actor.idx:
                        break
                    if abs(actor.pos-other.pos)<(actor.collision_radius+other.collision_radius):
                        have_collision = True
                        direction = actor.pos-other.pos
                        direction = direction/abs(direction)
                        contact = 0.5*(actor.pos + other.pos +
                                (actor.collision_radius-other.collision_radius)*normdir)
                        
                        actor.pos = contact + (actor.collision_radius+fe)*direction
                        other.pos = contact - (other.collision_radius+fe)*direction
            cnt += 1 
        # update misc actors ( bonuses, treats,...) 


        # update followers
        for bird in self.birds:
            cat = bird.get_followers_category()
            followers = self.pigeons.get_safe(cat)
            # swarm update
            #get_dest = lambda x: bird.pos # the simpler possible, no vis check
            get_dest = lambda e: e.following_get_dest()
            weights = [ 0.5, 0.5, 0.5 ]
            self.swarm_update( followers, get_dest, weights)

        # update lone pigeons (see if it begin to follow)
        lones = self.pigeons.get_safe('idle')
        for actor in lones:
            candidates = []
            for b in self.birds:
                dist , dest = actor.can_follow(b)
                if dist:
                    candidates.append((dist,dest,b))
            if candidates:
                candidates.sort(key = lambda x: x[0])
                actor.set_state('following', candidates[0][1], candidates[0][2])


        # handle interactions pusher line with pigeons
        line = self.pusher_line
        followers_cat = [ e.get_followers_category() for e in self.birds ]
        categories = [ 'idle', 'commissioned' ] + followers_cat
        for cat in categories:
            pigeons = self.pigeons.get_safe(cat)
            for pigeon in pigeons:
                if line.is_pigeon_touch(pigeon):
                    line.handle_pigeon_touch(pigeon)

        # handle interactions goal line with pigeons
        line = self.goal_line
        categories = [ 'commissioned' ] + followers_cat
        for cat in categories:
            pigeons = self.pigeons.get_safe(cat)
            for pigeon in pigeons:
                if line.is_pigeon_touch(pigeon):
                    line.handle_pigeon_touch(pigeon)


        # delete actors to the left of self.ox
        left_side = self.ox
        idx_list = [ idx for idx in actors ]
        for idx in idx_list:
            if actors[idx].right_side()<left_side:
                self.remove_actor(actors[idx])
        # problems can arise when an actor is in a list. Make the list over idx,
        # not the actors, and check if idx is in world.actors before proceed
                

        # detect end_of_stage :
        #   no idle pigeons (in any teams)
        #   no goalized pigeons ()
        #   no followers ()
        bContinue = False           
        for cat in [ 'idle', 'goalized', 'commissioned']:
            if self.pigeons.catlen(cat)>0:
                bContinue = True
                break
        if bContinue:
            for bird in self.birds:
                if self.pigeons.catlen(bird.get_followers_category())>0:
                    bContinue = True
                    break
        if not bContinue:
            # level ends
            self.game_ends(True)

##        for cat in self.pigeons.categories:
##            print cat, self.pigeons.catlen(cat)
        
            
        

        # detect game over
        
    def swarm_update(self, members, get_dest, weights):
##        posibilities for get_dest:
##            followers, -> leader.pos
##            followers -> leader.pos if visible, else pigeon.pos for the pigeon
##                         most near to leader that can see leader and e
##            followers -> leader.pos if visible, else last seen leader.pos
##            commissioned -> leader position at quack_unfollow event
##            commissioned -> bucket pos assigned at quack_unfollow event

        in_sight = self.terrain.is_visible
        cutout_dist = (gg.pigeon_r)*2.5
        cutout_vel = gg.pigeon_fastness
        w0, w1, w2 = weights[:3]
        cnt = 0
        for e in members:
            if e.next_swarm_time > self.time:
                continue
            e.next_swarm_time = self.time + gg.swarm_reschedule_time

            # follower component
            dest = get_dest(e)
            if dest is None:
                e.set_state('idle')
                continue
            else:
                mov = dest - e.pos
                dist = abs(mov)
                if dist<fe:
                    v1 = complex(0,0)
                else:
                    v1 = mov / dist * gg.pigeon_fastness

            # dont-fly-too-near component
            v2 = complex(0,0)
            for other in members:
                if other is e:
                    continue
                if abs(e.pos - other.pos)>cutout_dist:
                    continue
                #? using a small cutout_dist probably the vis test can be
                # spared
                #if in_sight(e.pos, other.pos):
                v2 -= (e.pos-other.pos) # pareceria que debiera ser al reves ?
            print 'repeller:' , v2
            v2 = -v2

##            # try-to-match-vel component - make distance limited also ?
##            visibles = [ other in members if e!=other and in_sight(e, other) ] 
##            v3 = 0
##            for other in visibles:
##                v3 += other.vel
##            v3 = v3/len(visibles)

            # compose - the bounce terrain info comes from e.vel
##            vel = w0 * e.vel + w1 * v1 + w2 * v2 + w3*v3 # wi weights
            vel = w0 * e.vel + w1 * v1 + w2 * v2 # wi weights
            # clamp and set
            mv = abs(vel)
            if mv>cutout_vel:
                e.vel = vel/mv*cutout_vel
            else:
                e.vel = vel

            cnt +=1
            if cnt>2:
                # limit the number of pigeon followers updated in each frame; it
                # is hard on cpu
                break
        

# TODO: dar score tambien por avance x y quizas recogidas bonus. ( o algun
# bonus puede ser multiplicador)

# register events generated by World
World.register_event_type('on_actor_deleted')
World.register_event_type('on_actor_added')
World.register_event_type('on_actor_changed')
World.register_event_type('on_stats_changed')
World.register_event_type('on_help_request')
World.register_event_type('on_help_end')
World.register_event_type('on_pov_changed')
World.register_event_type('on_game_over')
World.register_event_type('on_next_stage')
